home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / arm / util / conf.py < prev    next >
Encoding:
Python Source  |  2012-05-18  |  11.5 KB  |  338 lines

  1. """
  2. This provides handlers for specially formatted configuration files. Entries are
  3. expected to consist of simple key/value pairs, and anything after "#" is
  4. stripped as a comment. Excess whitespace is trimmed and empty lines are
  5. ignored. For instance:
  6. # This is my sample config
  7.  
  8. user.name Galen
  9. user.password yabba1234 # here's an inline comment
  10. user.notes takes a fancy to pepperjack chese
  11. blankEntry.example
  12.  
  13. would be loaded as four entries (the last one's value being an empty string).
  14. If a key's defined multiple times then the last instance of it is used.
  15. """
  16.  
  17. import threading
  18.  
  19. from util import log
  20.  
  21. CONFS = {}  # mapping of identifier to singleton instances of configs
  22. CONFIG = {"log.configEntryNotFound": None,
  23.           "log.configEntryTypeError": log.NOTICE}
  24.  
  25. def loadConfig(config):
  26.   config.update(CONFIG)
  27.  
  28. def getConfig(handle):
  29.   """
  30.   Singleton constructor for configuration file instances. If a configuration
  31.   already exists for the handle then it's returned. Otherwise a fresh instance
  32.   is constructed.
  33.   
  34.   Arguments:
  35.     handle - unique identifier used to access this config instance
  36.   """
  37.   
  38.   if not handle in CONFS: CONFS[handle] = Config()
  39.   return CONFS[handle]
  40.  
  41. class Config():
  42.   """
  43.   Handler for easily working with custom configurations, providing persistence
  44.   to and from files. All operations are thread safe.
  45.   
  46.   Parameters:
  47.     path        - location from which configurations are saved and loaded
  48.     contents    - mapping of current key/value pairs
  49.     rawContents - last read/written config (initialized to an empty string)
  50.   """
  51.   
  52.   def __init__(self):
  53.     """
  54.     Creates a new configuration instance.
  55.     """
  56.     
  57.     self.path = None        # location last loaded from
  58.     self.contents = {}      # configuration key/value pairs
  59.     self.contentsLock = threading.RLock()
  60.     self.requestedKeys = set()
  61.     self.rawContents = []   # raw contents read from configuration file
  62.   
  63.   def getValue(self, key, default=None, multiple=False):
  64.     """
  65.     This provides the currently value associated with a given key. If no such
  66.     key exists then this provides the default.
  67.     
  68.     Arguments:
  69.       key      - config setting to be fetched
  70.       default  - value provided if no such key exists
  71.       multiple - provides back a list of all values if true, otherwise this
  72.                  returns the last loaded configuration value
  73.     """
  74.     
  75.     self.contentsLock.acquire()
  76.     
  77.     if key in self.contents:
  78.       val = self.contents[key]
  79.       if not multiple: val = val[-1]
  80.       self.requestedKeys.add(key)
  81.     else:
  82.       msg = "config entry '%s' not found, defaulting to '%s'" % (key, str(default))
  83.       log.log(CONFIG["log.configEntryNotFound"], msg)
  84.       val = default
  85.     
  86.     self.contentsLock.release()
  87.     
  88.     return val
  89.   
  90.   def get(self, key, default=None):
  91.     """
  92.     Fetches the given configuration, using the key and default value to hint
  93.     the type it should be. Recognized types are:
  94.     - logging runlevel if key starts with "log."
  95.     - boolean if default is a boolean (valid values are 'true' and 'false',
  96.       anything else provides the default)
  97.     - integer or float if default is a number (provides default if fails to
  98.       cast)
  99.     - list of all defined values default is a list
  100.     - mapping of all defined values (key/value split via "=>") if the default
  101.       is a dict
  102.     
  103.     Arguments:
  104.       key      - config setting to be fetched
  105.       default  - value provided if no such key exists
  106.     """
  107.     
  108.     isMultivalue = isinstance(default, list) or isinstance(default, dict)
  109.     val = self.getValue(key, default, isMultivalue)
  110.     if val == default: return val
  111.     
  112.     if key.startswith("log."):
  113.       if val.upper() == "NONE": val = None
  114.       elif val.upper() in log.Runlevel.values(): val = val.upper()
  115.       else:
  116.         msg = "Config entry '%s' is expected to be a runlevel" % key
  117.         if default != None: msg += ", defaulting to '%s'" % default
  118.         log.log(CONFIG["log.configEntryTypeError"], msg)
  119.         val = default
  120.     elif isinstance(default, bool):
  121.       if val.lower() == "true": val = True
  122.       elif val.lower() == "false": val = False
  123.       else:
  124.         msg = "Config entry '%s' is expected to be a boolean, defaulting to '%s'" % (key, str(default))
  125.         log.log(CONFIG["log.configEntryTypeError"], msg)
  126.         val = default
  127.     elif isinstance(default, int):
  128.       try: val = int(val)
  129.       except ValueError:
  130.         msg = "Config entry '%s' is expected to be an integer, defaulting to '%i'" % (key, default)
  131.         log.log(CONFIG["log.configEntryTypeError"], msg)
  132.         val = default
  133.     elif isinstance(default, float):
  134.       try: val = float(val)
  135.       except ValueError:
  136.         msg = "Config entry '%s' is expected to be a float, defaulting to '%f'" % (key, default)
  137.         log.log(CONFIG["log.configEntryTypeError"], msg)
  138.         val = default
  139.     elif isinstance(default, list):
  140.       pass # nothing special to do (already a list)
  141.     elif isinstance(default, dict):
  142.       valMap = {}
  143.       for entry in val:
  144.         if "=>" in entry:
  145.           entryKey, entryVal = entry.split("=>", 1)
  146.           valMap[entryKey.strip()] = entryVal.strip()
  147.         else:
  148.           msg = "Ignoring invalid %s config entry (expected a mapping, but \"%s\" was missing \"=>\")" % (key, entry)
  149.           log.log(CONFIG["log.configEntryTypeError"], msg)
  150.       val = valMap
  151.     
  152.     return val
  153.   
  154.   def getStrCSV(self, key, default = None, count = None):
  155.     """
  156.     Fetches the given key as a comma separated value. This provides back a list
  157.     with the stripped values.
  158.     
  159.     Arguments:
  160.       key     - config setting to be fetched
  161.       default - value provided if no such key exists or doesn't match the count
  162.       count   - if set, then a TypeError is logged (and default returned) if
  163.                 the number of elements doesn't match the count
  164.     """
  165.     
  166.     confValue = self.getValue(key)
  167.     if confValue == None: return default
  168.     else:
  169.       confComp = [entry.strip() for entry in confValue.split(",")]
  170.       
  171.       # check if the count doesn't match
  172.       if count != None and len(confComp) != count:
  173.         msg = "Config entry '%s' is expected to be %i comma separated values" % (key, count)
  174.         if default != None and (isinstance(default, list) or isinstance(default, tuple)):
  175.           defaultStr = ", ".join([str(i) for i in default])
  176.           msg += ", defaulting to '%s'" % defaultStr
  177.         
  178.         log.log(CONFIG["log.configEntryTypeError"], msg)
  179.         return default
  180.       
  181.       return confComp
  182.   
  183.   def getIntCSV(self, key, default = None, count = None, minValue = None, maxValue = None):
  184.     """
  185.     Fetches the given comma separated value, logging a TypeError (and returning
  186.     the default) if the values arne't ints or aren't constrained to the given
  187.     bounds.
  188.     
  189.     Arguments:
  190.       key      - config setting to be fetched
  191.       default  - value provided if no such key exists, doesn't match the count,
  192.                  values aren't all integers, or doesn't match the bounds
  193.       count    - checks that the number of values matches this if set
  194.       minValue - checks that all values are over this if set
  195.       maxValue - checks that all values are less than this if set
  196.     """
  197.     
  198.     confComp = self.getStrCSV(key, default, count)
  199.     if confComp == default: return default
  200.     
  201.     # validates the input, setting the errorMsg if there's a problem
  202.     errorMsg = None
  203.     baseErrorMsg = "Config entry '%s' is expected to %%s" % key
  204.     if default != None and (isinstance(default, list) or isinstance(default, tuple)):
  205.       defaultStr = ", ".join([str(i) for i in default])
  206.       baseErrorMsg += ", defaulting to '%s'" % defaultStr
  207.     
  208.     for val in confComp:
  209.       if not val.isdigit():
  210.         errorMsg = baseErrorMsg % "only have integer values"
  211.         break
  212.       else:
  213.         if minValue != None and int(val) < minValue:
  214.           errorMsg = baseErrorMsg % "only have values over %i" % minValue
  215.           break
  216.         elif maxValue != None and int(val) > maxValue:
  217.           errorMsg = baseErrorMsg % "only have values less than %i" % maxValue
  218.           break
  219.     
  220.     if errorMsg:
  221.       log.log(CONFIG["log.configEntryTypeError"], errorMsg)
  222.       return default
  223.     else: return [int(val) for val in confComp]
  224.  
  225.   def update(self, confMappings, limits = {}):
  226.     """
  227.     Revises a set of key/value mappings to reflect the current configuration.
  228.     Undefined values are left with their current values.
  229.     
  230.     Arguments:
  231.       confMappings - configuration key/value mappings to be revised
  232.       limits       - mappings of limits on numeric values, expected to be of
  233.                      the form "configKey -> min" or "configKey -> (min, max)"
  234.     """
  235.     
  236.     for entry in confMappings.keys():
  237.       val = self.get(entry, confMappings[entry])
  238.       
  239.       if entry in limits and (isinstance(val, int) or isinstance(val, float)):
  240.         if isinstance(limits[entry], tuple):
  241.           val = max(val, limits[entry][0])
  242.           val = min(val, limits[entry][1])
  243.         else: val = max(val, limits[entry])
  244.       
  245.       confMappings[entry] = val
  246.   
  247.   def getKeys(self):
  248.     """
  249.     Provides all keys in the currently loaded configuration.
  250.     """
  251.     
  252.     return self.contents.keys()
  253.   
  254.   def getUnusedKeys(self):
  255.     """
  256.     Provides the set of keys that have never been requested.
  257.     """
  258.     
  259.     return set(self.getKeys()).difference(self.requestedKeys)
  260.   
  261.   def set(self, key, value):
  262.     """
  263.     Stores the given configuration value.
  264.     
  265.     Arguments:
  266.       key   - config key to be set
  267.       value - config value to be set
  268.     """
  269.     
  270.     self.contentsLock.acquire()
  271.     self.contents[key] = [value]
  272.     self.contentsLock.release()
  273.   
  274.   def clear(self):
  275.     """
  276.     Drops all current key/value mappings.
  277.     """
  278.     
  279.     self.contentsLock.acquire()
  280.     self.contents.clear()
  281.     self.contentsLock.release()
  282.   
  283.   def load(self, path):
  284.     """
  285.     Reads in the contents of the given path, adding its configuration values
  286.     and overwriting any that already exist. If the file's empty then this
  287.     doesn't do anything. Other issues (like having insufficient permissions or
  288.     if the file doesn't exist) result in an IOError.
  289.     
  290.     Arguments:
  291.       path - file path to be loaded
  292.     """
  293.     
  294.     configFile = open(path, "r")
  295.     self.rawContents = configFile.readlines()
  296.     configFile.close()
  297.     
  298.     self.contentsLock.acquire()
  299.     
  300.     for line in self.rawContents:
  301.       # strips any commenting or excess whitespace
  302.       commentStart = line.find("#")
  303.       if commentStart != -1: line = line[:commentStart]
  304.       line = line.strip()
  305.       
  306.       # parse the key/value pair
  307.       if line and " " in line:
  308.         key, value = line.split(" ", 1)
  309.         value = value.strip()
  310.         
  311.         if key in self.contents: self.contents[key].append(value)
  312.         else: self.contents[key] = [value]
  313.     
  314.     self.path = path
  315.     self.contentsLock.release()
  316.   
  317.   def save(self, saveBackup=True):
  318.     """
  319.     Writes the contents of the current configuration. If a configuration file
  320.     already exists then merges as follows:
  321.     - comments and file contents not in this config are left unchanged
  322.     - lines with duplicate keys are stripped (first instance is kept)
  323.     - existing entries are overwritten with their new values, preserving the
  324.       positioning of in-line comments if able
  325.     - config entries not in the file are appended to the end in alphabetical
  326.       order
  327.     
  328.     If problems arise in writing (such as an unset path or insufficient
  329.     permissions) result in an IOError.
  330.     
  331.     Arguments:
  332.       saveBackup - if true and a file already exists then it's saved (with
  333.                    '.backup' appended to its filename)
  334.     """
  335.     
  336.     pass # TODO: implement when persistence is needed
  337.  
  338.